<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Mouse Stroke - Help</title>
<link type="text/css" rel="stylesheet" href="options.css" />
<script type="text/javascript" src="essentials.js"></script>
<script type="text/javascript" src="actions.js"></script>
<script type="text/javascript" src="stroke.js"></script>
<script type="text/javascript">
/* ID prefix of user defined strokes */
var UDPFX = 'userdef#';

/* the id of the selected stroke command */
var _stroke_id = null;

/* stroke path from stroke.js */
var _path;

/* config object */
var _cfg;

/* port to the background page */
var _port = chrome.extension.connect({name: 'stroke.html-config'});

/* distinguish built-in strokes and user defined strokes.
 * user defined strokes' IDs are prefixed with UDPFX, there are no conflicts
 * as long as built-in strokes' IDs don't use this prefix */
var iscus = function(id) {
	return !!id.match(new RegExp('^' + UDPFX));
}

/* convert id to a readable label */
var id2label = function(id) {
	if (iscus(id))
		return unescape(id.replace(new RegExp('^' + UDPFX), ''));
	else
		return id.replace(/\-/g, ' ').replace(/\w+/g, function(s) {
			return s.charAt(0).toUpperCase() + s.substring(1);
		});
}

/* convert label to id, only used by user defined strokes */
var label2id = function(label) {
	return UDPFX + escape(label);
}

/* pack stroke sequence into a space separated string */
var pack = function(seq) {
	return (!!seq && seq.join) ? seq.join(' ').trim() : '';
}

/* generate element IDs based on the user defined stroke id */
var cus_label_id = function(id) {
	return id + '-label';
}
var cus_row_id = function(id) {
	return id + '-row';
}

/* construct the config form */
var construct = function(cfg)
{
	var id, tb, tr, td, input, set, unset, cat;

	/* restore options */
	$get('trigger').value = cfg.trigger;
	$get('timeout').value = cfg.timeout;
	$get('minstep').value = cfg.minstep;
	$get('trail').value = cfg.trail;
	$get('suppress').checked = !!cfg.suppress;

	/* stroke categories will be flatted for easy referring */
	cfg.strokes = {};

	/* restore built-in stroke settings */
	tb = $get('stroke-tbody');
	for (id in cfg.categories) {
		tr = $create('tr');
		/* categories */
		tr.appendChild($create('th', {
			className: 'title',
			colSpan: 2,
			textContent: id2label(id)
		}));
		tb.appendChild(tr);

		/* strokes */
		cat = cfg.categories[id];
		for (id in cat) {
			tr = $create('tr');
			/* label */
			tr.appendChild($create('td', {
				className: 'label',
				textContent: id2label(id)
			}));
			/* stroke field */
			input = $create('input', {
				id: id,
				type: 'text',
				disabled: true,
				className: 'stroke-field',
				value: pack(cat[id])
			});
			/* set button */
			set = $create('button', {
				className: 'set-stroke-btn',
				'for': id,
				textContent: 'Set'
			});
			set.addEventListener('click', set_stroke, false);
			/* unset button */
			unset = $create('button', {
				className: 'unset-stroke-btn',
				'for': id,
				textContent: 'Unset'
			});
			unset.addEventListener('click', unset_stroke, false);

			td = $create('td', { className: 'field' });
			td.appendChild(input);
			td.appendChild(set);
			td.appendChild(unset);
			tr.appendChild(td);
			tb.appendChild(tr);

			/* flat categories, all operations on cfg.strokes
			 * should done by reference, so that changes can be
			 * reflected on their counterparts in cfg.categories */
			cfg.strokes[id] = cat[id];
		}
	}

	/* restore user defined stroke settings */
	tb = $get('userdef-tbody');
	for (id in cfg.customs) {
		tb.appendChild(cus_construct(id));
	}

	/* bind restore/save button listener */
	$get('restore').addEventListener('click', restore, false);
	$get('save').addEventListener('click', harvest, false);
}

/* create UI for user defined strokes */
var cus_construct = function(id) {
	var tr, td, span, input, set, unset, remove, stroke = _cfg.customs[id];

	tr = $create('tr', { id: cus_row_id(id) });
	td = $create('td', { className: 'label' });

	/* label text */
	span = $create('span', {
		id: cus_label_id(id),
		textContent: id2label(id)
	});
	td.appendChild(span)
	tr.appendChild(td);

	/* stroke field */
	input = $create('input', {
		id: id,
		type: 'text',
		disabled: true,
		className: 'stroke-field',
		value: pack(stroke)
	});
	/* set button */
	set = $create('button', {
		className: 'set-stroke-btn',
		'for': id,
		textContent: 'Set'
	});
	set.addEventListener('click', set_stroke, false);
	/* unset button */
	unset = $create('button', {
		className: 'unset-stroke-btn',
		'for': id,
		textContent: 'Unset'
	});
	unset.addEventListener('click', unset_stroke, false);
	/* remove button */
	remove = $create('button', {
		className: 'remove-userdef-btn',
		'for': id,
		textContent: 'Remove'
	});
	remove.addEventListener('click', remove_userdef, false);

	td = $create('td', { className: 'field' });
	td.appendChild(input);
	td.appendChild(set);
	td.appendChild(unset);
	td.appendChild(remove);
	tr.appendChild(td);

	return tr;
}

/* save settings */
var harvest = function()
{
	var i, el, customs;

	_cfg.trigger = parseInt($get('trigger').value);
	_cfg.timeout = parseInt($get('timeout').value);
	_cfg.minstep = parseInt($get('minstep').value);
	_cfg.trail = $get('trail').value;
	_cfg.suppress = $get('suppress').checked;

	/* if the label of a user defined stroke was changed, its internal
	 * order within _cfg.customs will also change, so we need to re-sort
	 * _cfg.customs by elements order here */
	for (el = $get('userdef-tbody').getElementsByClassName('stroke-field'),
		i = 0, customs = {};
		i < el.length;
		customs[el[i].id] = _cfg.customs[el[i++].id]);
	_cfg.customs = customs;

	/* save locally */
	_port.postMessage({ action: 'save', data: _cfg });
	alert('Save complete');
}

/* restore default settings */
var restore = function()
{
	if (!confirm('All settings will be reset, except user defined strokes, proceed?'))
		return;

	_port.postMessage({
		action: 'save',
		data: { customs: _cfg.customs, scripts: _cfg.scripts }
	});
	location.reload(true);
}

/* event handler for setting stroke */
var set_stroke = function(e)
{
	var is_custom, stroke;

	/* set the command id, so the stroke listener knows which stroke
	 * should be set */
	_stroke_id = e.target['for'];
	is_custom = iscus(_stroke_id);
	stroke = is_custom ? _cfg.customs[_stroke_id] : _cfg.strokes[_stroke_id];

	if (is_custom) {
		$get('custom-label').value = id2label(_stroke_id);
		$get('custom-script').value = _cfg.scripts[_stroke_id] || '';
		$show('custom-content');
	}
	/* display the overlay, waiting for input */
	$get('which-stroke').textContent = id2label(_stroke_id);
	$get('old-stroke').textContent = pack(stroke);
	$get('new-stroke').textContent = '';
	$show('overlay');
}

/* event handler for unsetting stroke */
var unset_stroke = function(e) {
	var k = e.target['for'],
		s = iscus(k) ? _cfg.customs[k] : _cfg.strokes[k];
	/* unset the stroke */
	$get(k).value = '';
	/* empty the stroke array */
	s.splice(0, s.length);
}

/* event handler for remove user defined stroke */
var remove_userdef = function(e) {
	var k = e.target['for'], tr;
	if (confirm('Remove user defined stroke: "' + id2label(k) + '"?')) {
		tr = $get(cus_row_id(k));
		tr.parentNode.removeChild(tr);
		delete _cfg.customs[k];
		delete _cfg.scripts[k];
	}
}

/* event handler for add new user defined stroke */
var add_userdef = function(e) {
	var id, i = 1, label = 'New User Defined Stroke ';

	do { id = label2id(label + i++) }
	while (!!_cfg.customs[id]);
	/* push a special new stroke to customs */
	_cfg.customs[id] = [];
	_cfg.customs[id].neo = true;
	/* tell a lie */
	e.target['for'] = id;
	set_stroke(e);
}

/* event handler for the confirm button */
var confirm_change = function(e) {
	var k, tr, label, script, newstk, is_custom = iscus(_stroke_id),
		bucket = is_custom ? _cfg.customs : _cfg.strokes,
		stroke = bucket[_stroke_id],
		id = _stroke_id;

	/* label and script */
	if (is_custom) {
		label = $get('custom-label').value.trim();
		script = $get('custom-script').value;
		if (!label || !script.trim()) {
			alert('Both [Label] and [Script] can not be empty');
			return;
		}
		/*if (!evaluate(script)) return;*/

		/* id does not equal to _stroke_id if label is changed */
		id = label2id(label);
		/* notify conflicts */
		if (_stroke_id != id && !!bucket[id] && !confirm('"' + label + '" already exists, overwrite?'))
			return;
	}

	/* update stroke setting */
	if (!!_path && !!_path.stroke) {
		newstk = pack(_path.stroke);
		/* check duplicates */
		for (k in bucket) {
			if (id != k && newstk == pack(bucket[k])) {
				if (confirm('[' + newstk + '] has been assigned to "' + id2label(k) + '", overwrite?')) {
					/* clear conflicted settings */
					$get(k).value = '';
					bucket[k].splice(0, bucket[k].length);
					break;
				} else {
					return;
				}
			}
		}
		/* operate on the array reference, replace array elements
		 * with elements of _path.stroke */
		stroke.splice.apply(stroke, [0, stroke.length].concat(_path.stroke));
	}

	/* handle label change, update page elements */
	if (_stroke_id != id || stroke.neo) {
		/* a new stroke, or label changed */
		bucket[id] = stroke;
		tr = $get(cus_row_id(_stroke_id));
		/* this row exists when the user confirmed overwriting an
		 * existing label */
		if (!$get(cus_row_id(id)))
			$get('userdef-tbody').insertBefore(cus_construct(id), tr);
		if (_stroke_id != id) {
			delete bucket[_stroke_id];
			delete _cfg.scripts[_stroke_id];
			/* tr is null if this is a new stroke */
			if (!!tr) tr.parentNode.removeChild(tr);
		}
		/* from now on _stroke_id is replaced by new id, later
		 * references to _stroke_id should be pointed to the new id */
		_stroke_id = id;
	}
	if (!!newstk || stroke.neo) $get(id).value = newstk || pack(stroke);

	if (is_custom) _cfg.scripts[id] = script;
	if (stroke.neo) delete stroke.neo;

	cancel_change(e);
}

/* event handler for the cancel button */
var cancel_change = function(e) {
	/* cancel setting stroke */
	if (!!_cfg.customs[_stroke_id] && _cfg.customs[_stroke_id].neo) {
		/* a new stroke is cancelled */
		delete _cfg.customs[_stroke_id];
		delete _cfg.scripts[_stroke_id];
	}
	$hide('overlay');
	$hide('custom-content');
	_stroke_id = null;
	_path = null;
}

/* evaluate scripts of user defined strokes */
var evaluate = function(script) {
	var ex, msg, line, sbox = $get('sandbox').contentWindow;
	if (true !== (ex = sbox.sandbox.call({}, {}, script))) {
		/* extract compiler messages */
		ex = ex.stack.split("\n");
		line = ex[1].replace(/.+actions\.js:\d+:(\d+).+$/, '$1');
		msg = "The compiler reported a script error.\n\n";
		msg += ex[0]/* + "\nAt line: " + (line ? line : '<unknown>')*/;
		msg += "\n\nProceed with errors?";

		return confirm(msg);
	}
	return true;
}

/* stroke.js will fire an update-config command to stroke.html at start up,
 * we listen to the response too */
chrome.extension.onRequest.addListener(function(req, sender, resp) {
	/* apply only once */
	if (!_cfg && 'update-config' == req.action) {
		construct(_cfg = req.data);
	}
	try { resp({}) } catch (e) {}
});

window.addEventListener('load', function() {
	var sbox;
	$get('confirm-btn').addEventListener('click', confirm_change, false);
	$get('cancel-btn').addEventListener('click', cancel_change, false);
	$get('add-custom-btn').addEventListener('click', add_userdef, false);
	/* prepare the sandbox for evaluating user script
	sbox = $get('sandbox').contentWindow;
	sbox.document.head.appendChild($create('script', {
		type: 'text/javascript', src: 'actions.js'
	}));
	sbox.parent = sbox;
	sbox.alert = sbox.confirm = function() {}*/
});

/* listen to the stroke detecting script */
document.addEventListener('set-stroke-event', function(ev) {
	if (!!_stroke_id && !!_cfg[iscus(_stroke_id) ? 'customs' : 'strokes'][_stroke_id]) {
		/* valid _stroke_id */
		$get('new-stroke').textContent = pack(ev.path.stroke);
		/* cache path */
		_path = ev.path;
	}
}, false);
</script>
<body>

<div id="overlay" class="overlay hidden">
 <h1 id="which-stroke"></h1>
 <p>Previous<br/>[ <span id="old-stroke" class="old-stroke"></span> ]</p>
 <p>Current<br/>[ <span id="new-stroke" class="new-stroke"></span> ]</p>
 <div class="form">
  <div id="custom-content" class="custom-content hidden">
   <p><em>Label</em> *<br/>
    <input id="custom-label" class="custom-label" type="text" maxlength="64"/>
   </p>
   <p><em>Script</em> *<br/>
    <textarea id="custom-script" class="custom-script" spellcheck="false"></textarea>
   </p>
  </div>
 </div>
 <p>
  <button id="confirm-btn">Confirm</button>
  <button id="cancel-btn">Cancel</button>
 </p>
</div>

<!--iframe id="sandbox" class="hidden"></iframe-->

<div id="root" class="root">
 <h1><a href="http://code.google.com/p/chrome-mouse-stroke/">Mouse Stroke</a></h1>

 <p>
  This is a Google Chrome extension which enables
  <a href="http://en.wikipedia.org/wiki/Mouse_gesture">mouse strokes</a>
  (also known as mouse gestures) in the browser. With this extension, you can
  perform common tasks by simply moving your mouse.
 </p>

 <ol class="eye-catcher">
  <li>Since version 1.7, Mouse Stroke has User Defined Strokes support. Hold
   down the <strong>Ctrl</strong> key while performing mouse strokes will
   trigger a customized stroke, there are some examples for you to try out.
   You can even define your own strokes if you know JavaScript.
   <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/FAQs#How_to_create_my_own_strokes?">Here</a>
   is a simple guide on how to do it.
   <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/UserDefinedStrokeGallery">Sharing</a>
   your strokes are highly appriciated.
  </li>
  <li>Mouse Stroke still doesn't work on Linux/Mac, even if you use LEFT mouse
   button as the stroke trigger. The reason is explained
   <a href="http://code.google.com/p/chromium/issues/detail?id=26465">here</a>.
   I've posted a possible fix at
   <a href="http://code.google.com/p/chromium/issues/detail?id=28226">Chromium's issue list</a>,
   but I'm not holding my breath for it.
  </li>
  <li>Note that due to the security restrictions of Google Chrome extension system,
   the extension works in regular web pages only (any <code>http://</code> or
   <code>https://</code> sites), which means your strokes won't work in pages
   like <code>chrome://newtab</code> or <code>chrome://extensions</code>.
  </li>
  <li><strong><em>Please notice</em></strong>:
   Mouse Stroke needs a nice icon, I'm not good at designing, see the current
   one ;), so if you'd like to help, please post your design
   <a href="http://code.google.com/p/chrome-mouse-stroke/issues/detail?id=15">here</a>.
   You can also email it to
   <code><a href="http://code.google.com/u/vigacmoe/">my-user-name</a>@gmail.com</code>
   if you prefer. Designs of the options page are also welcomed. Thank you.
   PS: you will not get any money payback.
  </li>
 </ol>

 <table id="option-table" class="option-table" cellpadding="0" cellspacing="0" border="0">

  <tbody id="option-tbody">
   <tr>
    <th class="title" colspan="2">Options</th>
   </tr>
   <tr>
    <td class="label">Trigger</td>
    <td class="field">
     <select id="trigger">
      <option value="0">Left Mouse Button</option>
      <!--option value="1">Middle Mouse Button</option-->
      <option value="2">Right Mouse Button</option>
     </select>
    </td>
   </tr>
   <tr>
    <td class="label">
     Stroke Timeout (Milliseconds)<br/>
     <em><sub>
      Try 1000 if you feel difficult to perform strokes
     </sub></em>
    </td>
    <td class="field">
     <input id="timeout" type="text"/>
    </td>
   </tr>
   <tr>
    <td class="label">Minimum Step (Pixels)</td>
    <td class="field">
     <input id="minstep" type="text"/>
    </td>
   </tr>
   <tr>
    <td class="label">Draw Trails</td>
    <td class="field">
     <select id="trail">
      <option value="">none</option>
      <option value="black" style="color:black">black</option>
      <option value="red" style="color:red">red</option>
      <option value="green" style="color:green">green</option>
      <option value="yellow" style="color:yellow">yellow</option>
      <option value="blue" style="color:blue">blue</option>
      <option value="magenta" style="color:magenta">magenta</option>
      <option value="cyan" style="color:cyan">cyan</option>
     </select>
    </td>
   </tr>
   <tr>
    <td class="label">Do nothing if I'm holding Alt</td>
    <td class="field">
     <input id="suppress" type="checkbox"/>
    </td>
   </tr>
  </tbody>

  <tbody id="stroke-tbody">
  </tbody>

  <tbody id="userdef-tbody">
   <tr>
    <th class="title" colspan="2">
     User Defined Strokes (Ctrl key + drawing strokes)
     <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/FAQs#How_to_create_my_own_strokes?">Guide</a> |
     <a href="http://code.google.com/p/chrome-mouse-stroke/wiki/UserDefinedStrokeGallery">Share</a>
    </th>
   </tr>
  </tbody>
  <tbody id="userdef-add-tbody">
   <tr>
    <th colspan="2"><button id="add-custom-btn">New Stroke</th>
   </tr>
  </tbody>

  <tbody>
   <tr>
    <th class="submit" colspan="2">
     <button id="restore">Restore to default</button>
     <button id="save">Save</button>
    </th>
   </tr>
  </tbody>

 </table>

 <hr/>

 <div id="footer" class="footer">
  <a href="http://code.google.com/p/chrome-mouse-stroke/">Home</a> |
  <a href="http://code.google.com/p/chrome-mouse-stroke/issues/list">Support</a> |
  <a href="http://code.google.com/u/vigacmoe/">Author</a>
 </div>

</div>
</body>
</html>